import { makeMap, no } from "shared/util";
import { isNonPhrasingTag } from "web/compiler/util";
import { unicodeRegExp } from "core/util/lang";

// Regular Expressions for parsing tags and attributes
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`);
const startTagClose = /^\s*(\/?)>/; /* \s匹配所有空白字符包括换行 */
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
const doctype = /^<!DOCTYPE [^>]+>/i;
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = /^<!\--/;
const conditionalComment = /^<!\[/;

// Special Elements (can contain anything)
export const isPlainTextElement = makeMap("script,style,textarea", true);
const reCache = {};

const decodingMap = {
  "&lt;": "<",
  "&gt;": ">",
  "&quot;": '"',
  "&amp;": "&",
  "&#10;": "\n",
  "&#9;": "\t",
  "&#39;": "'",
};
const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g;
/* #9: tab */
/* #10：换行 */
/* 其他的上面有 */
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g;

// #5992
const isIgnoreNewlineTag = makeMap("pre,textarea", true);
const shouldIgnoreFirstNewline = (tag, html) =>
  tag && isIgnoreNewlineTag(tag) && html[0] === "\n";

function decodeAttr(value, shouldDecodeNewlines) {
  const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr;
  return value.replace(re, (match) => decodingMap[match]);
}

export function parseHTML(html, options) {
  /* 存放html标签 ，用于检测非单标签是否有闭合标签*/
  const stack = [];
  const expectHTML = options.expectHTML;
  /* 是否单标签 */
  const isUnaryTag = options.isUnaryTag || no;
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no;
  /* 当前游标于哪个位置（相对于完整template而言） */
  let index = 0;
  /* last: 处理前的模板字符串 */
  let last, lastTag;
  /* 循环，通过正则将模板字符串拆分  */
  while (html) {
    last = html;
    // Make sure we're not in a plaintext content element like script/style
    /* 判断是否为 style script  */
    if (!lastTag || !isPlainTextElement(lastTag)) {
      /* 匹配文本结束 */
      /* 此处三种情况 */
      /* 1.textEnd > 0，说明以文本开头 */
      /* 2.textEnd < 0，说明template为纯文本 */
      /* 3.textEnd = 0，说明模板以标签开头 */
      let textEnd = html.indexOf("<");
      /* 当< 为0 说明在 < 之前没有文本 */
      if (textEnd === 0) {
        // 注释 , 只会在使用模板字符串的时候才会生效，通过html创建的模板无法获取注释
        if (comment.test(html)) {
          const commentEnd = html.indexOf("-->");
          /* 如果 匹配到的注释结尾 */
          if (commentEnd >= 0) {
            /* 通过传入的comment判断是否保留注释 */
            if (options.shouldKeepComment) {
              options.comment(
                /* <!-- 占四个字符 ， 所以从4开始 */
                html.substring(4, commentEnd),
                index,
                index + commentEnd + 3
              );
            }
            advance(commentEnd + 3);
            continue;
          }
        }

        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        /* 是否为条件注释例如ie的 <![ */
        if (conditionalComment.test(html)) {
          /* 匹配结束位置 */
          const conditionalEnd = html.indexOf("]>");
          /* 结束位置大于0时为有效注释 */
          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2);
            continue;
          }
        }

        // Doctype:
        const doctypeMatch = html.match(doctype);
        if (doctypeMatch) {
          advance(doctypeMatch[0].length);
          continue;
        }

        // End tag:
        /* 匹配结束标签,只能以</tag>格式开头 */
        const endTagMatch = html.match(endTag);
        if (endTagMatch) {
          /* 保留当前游标 */
          const curIndex = index;
          /* 游标向后推 结束标签长度的位置 */
          advance(endTagMatch[0].length);
          /* endTagMatch[1]:标签名 */
          /* curIndex: 上一步游标位置 */
          /* index: 当前游标位置 */
          parseEndTag(endTagMatch[1], curIndex, index);
          continue;
        }

        // 匹配开始标签，属性，并且以对象形式返回，如果标签没有闭合，此标签不会做处理
        const startTagMatch = parseStartTag();
        if (startTagMatch) {
          /* 调用 options.start 创建语法树 */
          handleStartTag(startTagMatch);
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1);
          }
          continue;
        }
      }

      let text, rest, next;
      /* 文本节点 */
      if (textEnd >= 0) {
        rest = html.slice(textEnd);
        while (
          !endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.test(rest)
        ) {
          // < in plain text, be forgiving and treat it as text
          next = rest.indexOf("<", 1);
          if (next < 0) break;
          textEnd += next;
          rest = html.slice(textEnd);
        } /* 分割 */
        text = html.substring(0, textEnd);
      }
      /* 模板为纯文本时 */
      if (textEnd < 0) {
        text = html;
      }

      if (text) {
        advance(text.length);
      }

      if (options.chars && text) {
        options.chars(text, index - text.length, index);
      }
    } else {
      let endTagLength = 0;
      const stackedTag = lastTag.toLowerCase();
      const reStackedTag =
        reCache[stackedTag] ||
        (reCache[stackedTag] = new RegExp(
          "([\\s\\S]*?)(</" + stackedTag + "[^>]*>)",
          "i"
        ));
      const rest = html.replace(reStackedTag, function (all, text, endTag) {
        endTagLength = endTag.length;
        if (!isPlainTextElement(stackedTag) && stackedTag !== "noscript") {
          text = text
            .replace(/<!\--([\s\S]*?)-->/g, "$1") // #7298
            .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, "$1");
        }
        if (shouldIgnoreFirstNewline(stackedTag, text)) {
          text = text.slice(1);
        }
        if (options.chars) {
          options.chars(text);
        }
        return "";
      });
      index += html.length - rest.length;
      html = rest;
      parseEndTag(stackedTag, index - endTagLength, index);
    }
    /* last未处理前的template，html是这一次循环后的模板 */
    /* 如果上一次和这一次处理后的模板字符串相等，就说明模板字符串有问题 */
    if (html === last) {
      options.chars && options.chars(html);
      if (
        process.env.NODE_ENV !== "production" &&
        !stack.length &&
        options.warn
      ) {
        options.warn(`Mal-formatted tag at end of template: "${html}"`, {
          start: index + html.length,
        });
      }
      break;
    }
  }

  /* 此处处理一种特殊情况，当模板已经处理完毕，正常来说 stack中应该一个元素都不剩 */
    /* 除非模板传入有问题，例： */
    /* <div><div></div> */
    /* 上面模板处理后，第一个<div>会被留在stack中,因为唯一个 </div>已经被 第二个<div>使用*/
  parseEndTag();

  /**
   * @description: 游标后移，并且对html模板进行切割
   * @param {*} n /* 游标向后移的步长，模板切割的起始位置 */
   * @return {*}
   * @re_desc: void
   */
  function advance(n) {
    index += n;
    html = html.substring(n);
  }
  /**
   * @description: 将开始标签以对象的形式展示，并且对属性进行匹配,最后将处理后的对象进行返回
   * @param {*}
   * @return {*}
   */
  function parseStartTag() {
    /* 匹配开始标签 */
    /* start： ["<span", "span", index: 0, input: "<span></span>", groups: undefined] */
    /* 正则： /^<((?:[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)/ */
    const start = html.match(startTagOpen);
    /* 如果不以<开头，万事皆休 */
    if (start) {
      const match = {
        tagName: start[1],
        attrs: [],
        start: index,
      };
      advance(start[0].length);
      /* end：正则匹配结束标签后的结果 */
      /* attr: 正则匹配后的属性 */
      let end, attr;
      /* 此while循环主要处理的标签上的属性 */
      while (
        /* 匹配是否以闭合标签开头 */
        /* 正则为：/^\s*(\/?)>/ */
        !(end = html.match(startTagClose)) &&
        (attr =
          /* 匹配动态属性 v-bind @event v-html .... ,          */
          /*  ["v-html='test'", "v-html", "=", undefined, "test", undefined, index: 0, input: "v-html='test'", groups: undefined] */
          /* 正则为：/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ */
          html.match(dynamicArgAttribute) ||
          html.match(
            /* 匹配普通属性["class='hahhaha'", "class", "=", undefined, "hahhaha", undefined, index: 0, input: "class='hahhaha'", groups: undefined] */
            /* 正则为： /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ */
            attribute
          ))
      ) {
        /* 属性开始位置 */
        attr.start = index;
        /* 属性开始后推 */
        advance(attr[0].length);
        /* 属性结束位置 */
        attr.end = index;
        match.attrs.push(attr);
      }
      /* 如果存在闭合 */
      if (end) {
        /* 如果match是单标签，end[1]应该为 / ,反之 空字符 */
        match.unarySlash = end[1];
        /* 游标向后移动，模板继续切割 结束标签个长度 */
        advance(end[0].length);
        /* 记录此次头标签结束位置 */
        match.end = index;
        return match;
      }
    }
  }
  /**
   * @description: 
   * @param {*} match：{tagName,attr,start}
   * @return {*}
   * @re_desc: void
   */
  function handleStartTag(match) {
    /* 标签名 */
    const tagName = match.tagName;
    /* 闭合 */
    const unarySlash = match.unarySlash; 

    if (expectHTML) {
      if (lastTag === "p" && isNonPhrasingTag(tagName)) {
        parseEndTag(lastTag);
      }
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        parseEndTag(tagName);
      }
    }
    /* 是否为单标签 parseStartTag方法中定义*/
    const unary = isUnaryTag(tagName) || !!unarySlash;
    /* 属性长度，换而言之就是有多少个属性 */
    const l = match.attrs.length;
    /* 创建一个长度为l的空数组 */
    const attrs = new Array(l);
    /* 处理属性 为 {name:"href",value:"baidu.com"} */
    for (let i = 0; i < l; i++) {
      /*args: 第i个属性 */
      /* 此时args应该长这样： ["class='hahhaha'", "class", "=", undefined, "hahhaha", undefined, index: 0, input: "class='hahhaha'", groups: undefined]*/
      const args = match.attrs[i];
      /* undefined, "hahhaha", undefined */
      /* 获取的是属性值 */
      const value = args[3] || args[4] || args[5] || "";
      const shouldDecodeNewlines =
        tagName === "a" && args[1] === "href"
          ? options.shouldDecodeNewlinesForHref
          : options.shouldDecodeNewlines;
          /* 此处格式为 {name:"class",value:"hahhaha"} */
      attrs[i] = {
        name: args[1],
        /* &#39; &nbsp; ... 这类字符转义 */
        value: decodeAttr(value, shouldDecodeNewlines),
      };
      if (process.env.NODE_ENV !== "production" && options.outputSourceRange) {
        attrs[i].start = args.start + args[0].match(/^\s*/).length;
        attrs[i].end = args.end;
      }
    }
    /* 非 单标签 ，入栈 */
    if (!unary) {
      stack.push({
        tag: tagName,
        lowerCasedTag: tagName.toLowerCase(),
        attrs: attrs,
        start: match.start,
        end: match.end,
      });
      lastTag = tagName;
    }

    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end);
    }
  }

  function parseEndTag(tagName, start, end) {
    /* lowerCasedTagName 小写标签名 */
    /* pos会有一种特殊情况，例： */
    /* 模板为： <div>
                  </span>
                </div> */
    /* span没有开始标签，pos会为 -1 */
    let pos, lowerCasedTagName;
    if (start == null) start = index;
    if (end == null) end = index;

    // Find the closest opened tag of the same type
    if (tagName) {
      lowerCasedTagName = tagName.toLowerCase();
      /* 先获取标签在stack中的位置 */
      for (pos = stack.length - 1; pos >= 0; pos--) {
        if (stack[pos].lowerCasedTag === lowerCasedTagName) {
          break;
        }
      }
    } else {
      // If no tag name is provided, clean shop
      pos = 0;
    }
    /* 当传入的模板有误，例： */
    /* <div>
        <span>
    </div> */
    /* 上面span没有结束标签 */
    /* 当代码执行到这里，说明模板已经解析到了</div> 的位置 */
    /* 以下代码作用就是检测是否有结束标签 */
    if (pos >= 0) {
      // Close all the open elements, up the stack
      for (let i = stack.length - 1; i >= pos; i--) {
        if (
          process.env.NODE_ENV !== "production" &&
          /* 如果 i > pos 就说明stack最后一个标签和此次结束标签名不一样 */
          (i > pos || !tagName) &&
          options.warn
        ) {
          /* 可以判断出stack中最后一个标签是没有结束标签的 */
          /* 会依次循环，直到tagName 与  stack中tag匹配时，也就是pos的位置 */
          options.warn(`tag <${stack[i].tag}> has no matching end tag.`, {
            start: stack[i].start,
            end: stack[i].end,
          });
        }
        if (options.end) {
          options.end(stack[i].tag, start, end);
        }
      }

      // 将没匹配到闭合标签的元素从栈中弹出
      stack.length = pos;
      /* stack中最后一个标签 */
      lastTag = pos && stack[pos - 1].tag;
    } else if (lowerCasedTagName === "br") {
      if (options.start) {
        options.start(tagName, [], true, start, end);
      }
    } else if (lowerCasedTagName === "p") {
      if (options.start) {
        options.start(tagName, [], false, start, end);
      }
      if (options.end) {
        options.end(tagName, start, end);
      }
    }
  }
}
